// 
//   Copyright © 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
//  
//   This program is free software: you can redistribute it and/or modify
//   it under the terms of the GNU Affero General Public License as
//   published by the Free Software Foundation, either version 3 of the
//   License, or (at your option) any later version.
//  
//   This program is distributed in the hope that it will be useful,
//   but WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   GNU Affero General Public License for more details.
//  
//   You should have received a copy of the GNU Affero General Public License
//   along with this program.  If not, see <http://www.gnu.org/licenses/>.
//  
//  


using System;
using System.IO;
using System.Collections.Generic;

using Anculus.Core;

using Galaxium.Protocol.Xmpp.Library.Xml;
using Galaxium.Protocol.Xmpp.Library.Core;
using Galaxium.Protocol.Xmpp.Library.Extensions;

namespace Galaxium.Protocol.Xmpp.Library.Streams
{
	public enum InitiateFailureReason { NoValidStreams, BadProfile, Rejected, Timeout, Unknown }

	public abstract class StreamInitiator
	{
		private Client _client;
		private JabberID _uid;

		public string Profile { get; private set; }
		public string StreamID { get; private set; }
		
		private List<string> _manager_seq;
		private Dictionary<string, IBytestreamManager> _managers;
		
		protected StreamInitiator (Client client, JabberID uid, string profile, string sid)
		{
			_manager_seq = new List<string> ();
			_managers = new Dictionary<string, IBytestreamManager> ();
			
			_client = client;
			_uid = uid;
			Profile = profile;
			StreamID = sid;
		}
		

		private IEnumerable<ListOption> CreateOptions ()
		{
			var result = new List<ListOption> ();
			foreach (var method in _manager_seq)
				result.Add (new ListOption () { Value = method });
			return result;
		}
		
		protected void Initiate (string mime, IEnumerable<Element> profile_data)
		{
			var query = new Iq (IqType.Set, "si", Namespaces.StreamInitiation);
			query.To = _uid;
			query.Query ["id"] = StreamID;
			query.Query ["mime-type"] = mime;
			query.Query ["profile"] = Profile;
			
			foreach (var elm in profile_data)
				query.Query.AppendChild (elm);
			
			var field = new DataField ("stream-method", DataFieldType.ListSingle);
			field.SetOptions (CreateOptions ());
			var form = new DataForm (FormAction.Form, null);
			form.AddField (field);
			var feature = new Element ("feature", Namespaces.FeatureNegotiation);
			feature.AppendChild (form.ToXml (false));
			query.Query.AppendChild (feature);
			
			_client.SendQuery (query, InitiateResponseHandler);
			
			// TODO: a way to cancel the request
		}
		
		private void InitiateResponseHandler (Iq response)
		{
			try	{
				if (response == null)
					HandleErrorResponse (null);
				else if (response.IsError)
					HandleErrorResponse (response.Error);
				else
					HandleSuccessResponse (response.Query);
			}
			catch (Exception e)	{
				Log.Error (e, "Exception occured while handling stream initiate response"
				           + "\n{0}", response.ToString ());
				OnInitiateFailure (InitiateFailureReason.Unknown,
				                   "Unexpected error. See log for further details.");
				return;
			}
		}
		
		private void HandleErrorResponse (StanzaError error)
		{
			if (error == null) {
				OnInitiateFailure (InitiateFailureReason.Unknown, "Disconnected");
				return;
			}
			
			var reason = InitiateFailureReason.Unknown;
			switch (error.GetCondition (Namespaces.StreamInitiation)) {
				case "no-valid-streams": reason = InitiateFailureReason.NoValidStreams; break;
				case "bad-profile": reason = InitiateFailureReason.BadProfile; break;
				case "forbidden": reason = InitiateFailureReason.Rejected; break;
			}
			var text = error.Description;
			OnInitiateFailure (reason, text);
		}
		
		private void HandleSuccessResponse (Element query)
		{
			IBytestreamManager manager = null;
			var data = new List<Element> ();
			
			foreach (var elm in query) {
				try {
					if (elm.Name == "feature" && elm.Namespace == Namespaces.FeatureNegotiation) {
						var form = new DataForm (elm.FirstChild ("x", Namespaces.DataForms), null);
						manager = _managers [form ["stream-method"].GetString ()];
					}
					else if (elm.Namespace == Profile)
						data.Add (elm);
				}
				catch { Log.Error ("Element handling unsuccessful:\n" + elm.ToString ()); }
			}
			
			if (manager == null) {
				// invalid stream method
				OnInitiateFailure (InitiateFailureReason.NoValidStreams,
				                   "Entity doesn't support any proposed methods.");
				return;
			}
			
			manager.RequestStream (_uid, StreamID, (StreamRequestResult result, Stream stream) => {
				switch (result) {
					case StreamRequestResult.Rejected:
						OnInitiateFailure (InitiateFailureReason.Rejected,
						                   "Entity rejected the stream.");
						break;
					case StreamRequestResult.Success:
						OnInitiated (stream, data);
						break;
					case StreamRequestResult.TimedOut:
						OnInitiateFailure (InitiateFailureReason.Timeout,
						                   "Request timed out.");
						break;
					case StreamRequestResult.Unsupported:
						OnInitiateFailure (InitiateFailureReason.NoValidStreams,
						                   "Entity doesn't support any proposed methods.");
						break;
					case StreamRequestResult.UnknownFailure:
						OnInitiateFailure (InitiateFailureReason.Unknown,
						                   "Unknown error occured.");
						break;
				}
			});
		}
		
		protected abstract void OnInitiateFailure (InitiateFailureReason reason, string text);
		protected abstract void OnInitiated (Stream stream, IList<Element> data);
		
		public void AddBytestreamMethod (IBytestreamManager manager)
		{
			if (_client != null && manager.Client == null)
				manager.Attach (_client);
			else if (_client != manager.Client)
				throw new ArgumentException ("Manager doesn't belong to the same client.");
			
			_managers.Add (manager.Identifier, manager);
			_manager_seq.Add (manager.Identifier);
		}
	}
}
